1 | import objgraph |
Python的对象模型
Python中一切都是对象,而变量则是对对象的引用:
- 对象:分配的一块内存,有足够空间去表示他们所代表的值;
- 变量:是命名空间(字典)中的key,指向它所引用的对象;
- 引用:变量到对象的连接,以指针的形式实现;
关于变量-对象-引用之间的关系:
- 变量只能引用对象,绝不会引用其其他变量;
- 一个变量同一时刻只能引用一个对象,但一个对象同时间可以被多个变量引用;
- 容器对象可以连接到子对象;
本文用到的辅助工具:
- id(object): 返回对象的内存地址;
- a is b: 判断两个对象是否为同一对象;
- a == b: 判断两个对象是否等值;
- objgraph.show_refs(objects): 生成从对象objects开始的对象引用图例,参见文档;
赋值
Python中变量定义、函数定义、函数传参、类定义、模块导入等操作本质上都是赋值操作,遵循同样的赋值逻辑:Python中的赋值只是创建引用,不会拷贝对象。
a = b
C语言的赋值:”拷贝-写入”模式(拷贝b对象的值,写入a内存)
python语言的赋值:“解引用-创建引用”模式(将变量b解引用为对象,创建变量a到对象b的引用)
1 | b = "hello" |
4457019016 4457019016
True
1 | b ="no" |
4457019016 4417389320
False
变量赋值、对象原地修改
- 变量赋值:对变量赋值,只是使该变量指向了新的对象;
1 | a = [1,2,3] |
4457160136 4457160136
1 | a = {'a':22,'b':33} |
4457132464 4457160136
- 原地修改:对容器对象进行原地修改,只是改变了子对象的引用,不改变容器对象的地址
1 | a = [1,2,3] |
4457143432 4414302400
1 | a[0] = 11 |
4457143432 4414302400
按照是否支持原地修改,Python中的内置数据类型可分为两大类:
- 可变类型:支持原地修改,如列表、字典;
- 不可变类型:不支持原地修改,如元组、字符串等列表、字典以外的内置类型;
共享引用
共享引用:多个变量同时引用了同一个对象;
- 对其中一个变量赋值,不会影响到其他变量;
1 | a = b = c = [1,2] |
4454376648 4454376648 4454376648
1 | c = None |
4454376648 4454376648 4414019688
- 对其中一个进行原地修改则会同时改变其他变量;
1 | b[0]='change' |
4454376648 4454376648 4414019688
- Python会缓存复用小的整数(-5到256)和字符串以提高效率,不同版本缓存范围不同,缓存字符串的行为令人费解,尽量不要在应用程序中使用这个特性;
1 | # 缓存整数范围:[-5~256] |
整数缓存范围:[-5,256]
1 | # 缓存字符串长度范围:20以内 |
True
1 | const = 20 |
False
显式拷贝
赋值不会发生拷贝,如果想要生成新的副本则需要显式地进行拷贝。
浅拷贝
copy.copy(obj)通过拷贝生成一个新对象,新对象只拷贝了原对象的壳,但仍共享引用原对象的内容,也就是说新对象与原对象的id不同,但是新对象中的子对象与原始对象中的子对象id相同。
1 | import copy |
[[1, 2], [6, 5]] [[1, 2], [6, 5]]
4456953352 4457062536
4456953160 4456953160
- 对新旧对象的原地修改不会影响到另外一个对象
1 | a[1]='ni' |
[[1, 2], 'ni'] [[1, 2], [6, 5]]
- 对新旧对象的属性和内容进行原地修改则会影响到另一对象
1 | a[0][0]='aaa' |
[['aaa', 2], [6, 5]] [['aaa', 2], 'ni']
深拷贝
copy.deepcopy(obj),通过“递归拷贝”原对象生成新对象,新对象与原始对象除了内容相同外没有任何联系。
1 | a = [[1,2],[6,5]] |
[[1, 2], [6, 5]] [[1, 2], [6, 5]]
4457080328 4456172872
4457082376 4457081672
新旧对象互不影响
1 | b[1]='ni' |
[[1, 2], [6, 5]] [[1, 2], 'ni']
1 | a[0][0]='aaa' |
[['aaa', 2], [6, 5]] [[1, 2], 'ni']
隐式浅拷贝
除了使用显式拷贝来生成原对象的副本外,需要特别注意的是,Python内置的一些方法会“隐式”地对原始对象进行浅拷贝,比如类型转换函数、列表切片、运算符重载的+
*
,这往往会导致Python中某些看起来很奇怪的现象。
类型转换函数
1 | L = [[1],[2],[3]] |
[[1], [2], [3]] ([1], [2], [3])
4456981064 4457278776
4456898440 4456898440
1 | L[0]=99 |
[99, [2], [3]] ([9], [2], [3])
1 | L[0][0]=9 |
[[9], [2], [3]] ([9], [2], [3])
合并重复操作
+
*
用于列表,相当于浅拷贝了原始对象,产生多个副本,新副本中的每个子对象都还是原始对象中的子对象,任何对这些子对象的原地修改都会自动应用到所有新的副本中。
1 | L=[0,1] |
[0, 1] [0, 1, 0, 1]
1 | L=[0,1] |
[0, 1] [0, 1, 0, 1, 0, 1]
1 | L=[0,1] |
[0, 1] ['a', 'b'] [[0, 1], ['a', 'b']] [[0, 1], ['a', 'b'], [0, 1], ['a', 'b'], [0, 1], ['a', 'b']]
1 | L[0]=99 |
[99, 1] ['a', 'b'] [[99, 1], ['a', 'b']] [[99, 1], ['a', 'b'], [99, 1], ['a', 'b'], [99, 1], ['a', 'b']]
切片
切片虽然返回了新的对象,但是新对象中的子对象还是原始对象中的子对象
1 | L=[[0],[1],[2],[3],[4],[5]] |
[[0], [1], [2], [3], [4], [5]] [[1], [3], [5]]
对新生成的对象原地修改并不会改变原对象
1 | Q[0]='first' |
[[0], [1], [2], [3], [4], [5]] ['first', [3], [5]]
对新对象的元素进行原地修改会影响到原对象
1 | Q[1][0]=333 |
[[0], [1], [2], [333], [4], [5]] ['first', [333], [5]]
拷贝不可变对象
没有必要拷贝不可变对象,因为完全不用担心会不经意改动它们。
如果对不可变对象进行拷贝操作,仍然会得到原对象
1 | T=(1,2,3) |
(1, 2, 3) (1, 2, 3)
1 | T=([1],[2],[3]) |
([1], [2], [3]) ([1], [2], [3])
1 | T[1][0]='ni' |
([1], ['ni'], [3]) ([1], ['ni'], [3])
烤全羊
下面的例子融合了Python中各种常见的引用关系:
1 | li = [1,'string',('tuple',1),['list',3,('gh',4),{'i':'j',5:'k'}] |
1 | objgraph.show_refs([li], filename='sample-graph.png') |
下面例子反应了在Python自动缓存较小整数时的引用关系
1 | import copy |